package czopyk.model.persistence;

import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

import pl.czopyk.colt.math.ColtCompositeDistribution;
import pl.czopyk.colt.math.DistributionWrapper;
import cern.jet.random.AbstractDistribution;

import com.espertech.esper.collection.Pair;

import czopyk.model.cost.DistributionId;
import czopyk.model.factories.CenterFactory;
import czopyk.model.factories.Utils;
import czopyk.model.request.LoadClass;
import czopyk.model.structure.Center;
import czopyk.model.structure.CenterClass;

public class ModelSerializer implements ContentHandler {

	private static final Logger lgr = Logger.getLogger(ModelSerializer.class);

	// komponenty bieżące
	private Map<LoadClass, DistributionId> lcComponents = new HashMap<LoadClass, DistributionId>();

	// klasa i typ zadania
	private Map<String, LoadClass> idToRequestClassAndType = new HashMap<String, LoadClass>();

	// infrastruktura
	private Center gate;
	private final Map<String, Center> nodes = new HashMap<String, Center>();
	private final Set<Pair<String, String>> graph = new HashSet<Pair<String, String>>();
	private String gateId;

	// kompozyty rozkladow
	private String currentCompositeDistributionId;
	private List<Pair<BigDecimal, String>> currentCompositeDistributionComponents = new LinkedList<Pair<BigDecimal, String>>();

	private String currentLoadClassId;
	private String currentLoadClassType;
	private String currentLoadClassClazz;

	private void clean() {
		lcComponents.clear();
		idToRequestClassAndType.clear();

		// model
		nodes.clear();
		graph.clear();
		gate = null;
	}

	public void setDocumentLocator(Locator locator) {
	}

	public void startDocument() throws SAXException {
		clean();
	}

	public void endDocument() throws SAXException {

		if (!czopyk.model.factories.Locator.getLoadSelector().isSelectorReady()) {
			throw new SAXException(
					"Load selector not ready: load class probabilities do not sum to 1.0");
		}

	}

	private void build() {
		for (Pair<String, String> p : graph) {
			Center sc = nodes.get(p.getFirst());
			Center tc = nodes.get(p.getSecond());
			if (sc == null) {
				throw new IllegalStateException("No center " + p.getFirst()
						+ " found for edge: (" + p.getFirst() + "," + p.getSecond() + ").");
			} else if (tc == null) {
				throw new IllegalStateException("No center " + p.getSecond()
						+ " found for edge: (" + p.getFirst() + "," + p.getSecond() + ").");
			}
			sc.getAllNext().add(tc);
		}
		gate = nodes.get(gateId);
		if (gate == null) {
			throw new IllegalStateException(
					"Unable to set gate - probably 'gateId' undefined.");
		}

		czopyk.model.factories.Locator.getValidator().validate(gate);
		// rejestracja mapowania kosztu
		czopyk.model.factories.Locator.getCostMapper().validate();
	}

	public void startPrefixMapping(String prefix, String uri) throws SAXException {
	}

	public void endPrefixMapping(String prefix) throws SAXException {
	}

	public void startElement(String uri, String localName, String qName,
			final Attributes atts) throws SAXException {
		lgr.debug("Parsing element: " + localName + "[" + toString(atts) + "]");

		if ("distribution".equals(localName)) {
			String id = atts.getValue("id");

			czopyk.model.factories.Locator.getDistributionManager()
					.registerDistribution(id, Helper.buildDistribution(atts));
		} else if ("distributionComposition".equals(localName)) {
			String id = atts.getValue("id");
			currentCompositeDistributionId = id;
			currentCompositeDistributionComponents.clear();
		} else if ("distributionComponent".equals(localName)) {
			BigDecimal bd = new BigDecimal(atts.getValue("weight"));
			String componentId = atts.getValue("id");
			currentCompositeDistributionComponents.add(new Pair<BigDecimal, String>(
					bd, componentId));
		} else if ("component".equalsIgnoreCase(localName)) {
			// komponent zadania
			LoadClass loadClass = idToRequestClassAndType.get(atts.getValue("id"));
			Validate.notNull(loadClass,
					"Load class not found for id = '" + atts.getValue("id") + "'");
			lcComponents.put(loadClass,
					new DistributionId(atts.getValue("distributionId")));
		} else if ("loadClass".equals(localName)) {
			currentLoadClassType = atts.getValue("type");
			currentLoadClassClazz = atts.getValue("class");
			currentLoadClassId = atts.getValue("id");
		}
		// load
		else if ("users".equalsIgnoreCase(localName)) {
			czopyk.model.factories.Locator.getLoadGenerator().setUserCount(
					Integer.valueOf(atts.getValue("value")));
		} else if ("delay".equalsIgnoreCase(localName)) {
			czopyk.model.factories.Locator.getLoadGenerator().setLatency(
					new DistributionId(atts.getValue("distributionId")));
		} else if ("load".equalsIgnoreCase(localName)) {
			LoadClass loadClass = idToRequestClassAndType.get(atts
					.getValue("loadClassId"));

			if (loadClass == null) {
				throw new IllegalArgumentException("No load class defined under id '"
						+ atts.getValue("loadClassId") + "'");
			}

			if (!NumberUtils.isNumber(atts.getValue("prob"))) {
				throw new IllegalArgumentException("A 'prob' property must be a number");
			}

			czopyk.model.factories.Locator.getLoadSelector().registerClass(loadClass,
					new BigDecimal(atts.getValue("prob")));
		} else if ("randSeed".equalsIgnoreCase(localName)) {
			// ustawienie deterministycznych generatorów
			int seed = Integer.parseInt(atts.getValue("value"));
			Utils.setSeed(seed);
		}

		// infrastruktura
		else if ("center".equalsIgnoreCase(localName)) {
			final String id = atts.getValue("id");
			buildCenter(id, atts);
		} else if ("edge".equalsIgnoreCase(localName)) {
			String id1 = atts.getValue("sourceId");
			if (id1 == null) {
				throw new IllegalArgumentException("Edge sourceId cannot be null.");
			}
			String id2 = atts.getValue("targetId");
			if (id2 == null) {
				throw new IllegalArgumentException("Edge targetId cannot be null.");
			}
			graph.add(new Pair<String, String>(id1, id2));
		} else if ("gate".equalsIgnoreCase(localName)) {
			gateId = atts.getValue("id");
			if (gateId == null) {
				throw new IllegalArgumentException("Gate must specify 'gateId'.");
			}
		}

		else if ("cost".equalsIgnoreCase(localName)) {
			String centerClassString = atts.getValue("centerClass");
			String loadClassIdString = atts.getValue("loadClassId");
			String distributionIdString = atts.getValue("distributionId");
			String interferenceFactorString = atts.getValue("interferenceFactor");

			if (centerClassString == null) {
				throw new IllegalArgumentException(
						"Element 'cost' must declare attribute 'centerClass'");
			}
			if (loadClassIdString == null) {
				throw new IllegalArgumentException(
						"Element 'cost' must declare attribute 'loadClassId'");
			}
			if (distributionIdString == null) {
				throw new IllegalArgumentException(
						"Element 'cost' must declare attribute 'distributionId'");
			}
			if (interferenceFactorString != null
					&& !NumberUtils.isNumber(interferenceFactorString)) {
				throw new IllegalArgumentException(
						"Element 'cost' has attribute 'interferenceFactor' of improper type ('"
								+ interferenceFactorString + "'): double value expected");
			}

			CenterClass cc = new CenterClass(centerClassString);
			LoadClass lc = idToRequestClassAndType.get(loadClassIdString);

			// rejestracja mapowania kosztu
			czopyk.model.factories.Locator.getCostMapper().registerCostMapping(lc,
					cc, new DistributionId(distributionIdString));

			if (interferenceFactorString != null) {
				czopyk.model.factories.Locator.getInterferenceManager()
						.registerInterferenceFactor(lc, cc,
								Double.valueOf(interferenceFactorString));
			}
		}
	}

	private void buildCenter(final String id, Attributes atts) {
		lgr.debug("late init: " + toString(atts));
		if (id == null) {
			throw new IllegalArgumentException("Center id cannot be null.");
		}

		final String name = atts.getValue("name");

		final int cores = Integer.valueOf(atts.getValue("cores") != null
				&& StringUtils.isNumeric(atts.getValue("cores")) ? atts
				.getValue("cores") : "1");

		String typeString = atts.getValue("type") != null ? atts.getValue("type")
				.toUpperCase() : null;
		int type = CenterFactory.TYPE_COMMON;
		if (typeString != null) {
			if (typeString.contains("CACHE")) {
				type |= CenterFactory.TYPE_CACHE;
			}
			if (typeString.contains("LOAD_BALANCER")) {
				type |= CenterFactory.TYPE_LOAD_BALANCER;
			}
		}

		final Double hr = atts.getValue("hitRate") != null ? Double.valueOf(atts
				.getValue("hitRate")) : null;

		final CenterClass cc = new CenterClass(atts.getValue("centerClass"));

		nodes.put(
				id,
				czopyk.model.factories.Locator.getCenterFactory().getCenter(name, cc,
						cores, type, hr));
	}

	private String toString(Attributes atts) {
		StringBuilder sb = new StringBuilder();
		int i = 0;
		while (i < atts.getLength()) {
			sb.append(atts.getLocalName(i) + "=" + atts.getValue(i) + ", ");
			++i;
		}
		if (sb.length() > 1) {
			sb.deleteCharAt(sb.length() - 1);
			sb.deleteCharAt(sb.length() - 1);
		}
		return sb.toString();
	}

	public void endElement(String uri, String localName, String qName)
			throws SAXException {
		lgr.debug("ending element: " + localName);

		if ("loadClass".equalsIgnoreCase(localName)) {
			idToRequestClassAndType.put(
					currentLoadClassId,
					czopyk.model.factories.Locator.getRequestFactory()
							.registerRequestClass(currentLoadClassType,
									currentLoadClassClazz, lcComponents));
			lcComponents = new HashMap<LoadClass, DistributionId>();
		} else if ("distributionComposition".equals(localName)) {

			final Collection<Pair<BigDecimal, AbstractDistribution>> components = new LinkedList<Pair<BigDecimal, AbstractDistribution>>();

			for (Pair<BigDecimal, String> pair : currentCompositeDistributionComponents) {
				DistributionWrapper dw = czopyk.model.factories.Locator
						.getDistributionManager().getDistributionById(
								new DistributionId(pair.getSecond()));

				if (dw == null) {
					throw new IllegalArgumentException(
							"No component distribution with id = '" + pair.getSecond()
									+ "' found.");
				}

				components.add(new Pair<BigDecimal, AbstractDistribution>(pair
						.getFirst(), dw));
			}

			czopyk.model.factories.Locator.getDistributionManager()
					.registerDistribution(currentCompositeDistributionId,
							new DistributionWrapper() {
								private static final long serialVersionUID = 1L;
								{
									delegate = new ColtCompositeDistribution(components, Utils
											.getNewRandomEngine());
									offset = 0.0;
								}
							});
		}
	}

	public void characters(char[] ch, int start, int length) throws SAXException {
	}

	public void ignorableWhitespace(char[] ch, int start, int length)
			throws SAXException {
	}

	public void processingInstruction(String target, String data)
			throws SAXException {
	}

	public void skippedEntity(String name) throws SAXException {
	}

	private boolean ran = false;

	public Center getGate() {
		// budowa infrastruktury
		if (!ran) {
			build();
			ran = true;
		}
		return gate;
	}

}
